iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0
Software Development

Python十翼:與未來的自己對話系列 第 20

[Day20] 七翼 - Protocols:Sequence Protocol

  • 分享至 

  • xImage
  •  

七翼大綱

接下來三天,我們介紹Python三種常用的protocols。

  • [Day20]介紹Sequence Protocol。
  • [Day21]介紹Iteration Protocol。
  • [Day22]介紹Context Manager Protocol。

Sequence Protocol

根據Python docs的說明,sequence是一個實作有__len____getitem__,且能以整數作為index取值的iterable

Sequence protocol有時候也被稱作old-style iteration protocol

__len__

__len__必須回傳整數值。它除了讓我們可以使用len(obj)的語法來得知sequence的長度外,也會在沒有實作某些dunder method時,和__getitem__聯手,提供相同的功能。

__getitem__

__getitem__是一個有趣的dunder method,它讓我們能夠使用[]來取值,像list因此可以使用整數slice作為index來取值,而dict因此可以使用hashableobj作為key來取值。

__iter__的備案

__getitem__符合下列條件時,sequence可以在不實作__iter__的情況下,視為iterable

index會從0開始呼叫,當index在可取值範圍內回傳其值。當超出範圍時,raise IndexError

這個描述非常類似list,而的確我們也常使用list作為sequence所真正包含的容器。

__contains__的備案

__contains__是Python用來處理membership testdunder method。當使用in obj,而obj沒有實作__contains__時,會改使用__iter__。如果再沒有實作__iter__的話,會改使用__getitem__

__reversed__的備案

__reversed__一般被Python的built-in reversed所呼叫。當使用reversed(obj),而obj沒有實作__reversed__時,會使用__getitem_____len__來達成reversed的功能。

實例說明

# 01MySeq是一個實作有__getitem____len__class,所以my_seq可以使用[]來取值。

# 01
class MySeq:
    def __init__(self, iterable):
        self._list = list(iterable)

    def __len__(self):
        print('__len__ called')
        return len(self._list)

    def __getitem__(self, value):
        print(f'__getitem__ called, {value=}')
        try:
            return self._list[value]
        except Exception as e:
            print(type(e), e)
            raise


if __name__ == '__main__':
    my_seq = MySeq(range(3))

    print('*****test []*****')
    print(f'{my_seq[0]=}')  # 0
*****test []*****
__getitem__ called, value=0
my_seq[0]=0

由於我們將__getitem__內取值的任務delegatelist,所以符合前面「__iter__的備案」所述的條件,因此Python會將my_seq視為iterable

# 01
...
if __name__ == '__main__':
    ...
    print('*****test is an iterable*****')
    for item in my_seq:
        pass
*****test is an iterable*****
__getitem__ called, value=0
__getitem__ called, value=1
__getitem__ called, value=2
__getitem__ called, value=3
<class 'IndexError'> list index out of rang

我們也可以觀察,當index=3時,因為超過了my_seq能接收的範圍,self._list會報錯,而其錯誤型態的確為IndexError

# 01
...
if __name__ == '__main__':
    ...
    print('*****test in operator*****')
    print(f'{2 in my_seq=}')
*****test in operator*****
__getitem__ called, value=0
__getitem__ called, value=1
__getitem__ called, value=2
2 in my_seq=True

由於my_seq沒有實作__contains____iter__,所以Python會依靠__getitem__逐個取值,來比對2有沒有在my_seq中。

# 01
...
if __name__ == '__main__':
    ...
    print('*****test is reversible*****')
    for i in reversed(my_seq):  
        pass
*****test is reversible*****
__len__ called
__getitem__ called, value=2
__getitem__ called, value=1
__getitem__ called, value=0

由於my_seq沒有實作__reversed__,所以Python會同時使用__getitem____len__來達成reversed的功能。

collections.abc

Python的collections.abc中有SequenceMutableSequence兩種abstract base class,方便我們繼承使用。文件中有說明我們必須實作哪些dunder method,而根據這些dunder method,Python將能自動提供其它額外的method可以使用。

如果繼承Sequence的話,只需要實作__getitem____len__,就能額外獲得__contains____iter____reversed__indexcount

如果繼承MutableSequence的話,只需要實作__getitem____setitem____delitem____len__insert,就能獲得繼承
Sequence額外獲得的method加上appendreverseextendpopremove__iadd__

當日筆記

雖然只實作__getitem____len__,就可以作為很多dunder method的備案,但是依靠__getitem__逐個取值的效率是比較差的。所以如果可能的話,我們會建議針對各種dunder method實作比較有效率的邏輯。

參考資料

Code

本日程式碼傳送門


上一篇
[Day19] 六翼 - 導讀Descriptor HowTo Guide:Pure Python Equivalents
下一篇
[Day21] 七翼 - Protocols:Iteration Protocol
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言